﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Transactions;

using Ordinaire.Db;
using Ordinaire.Data;

namespace Ordinaire
{
    /// <summary>
    /// Represents a common data manager that provides common functions on hierarchical data structure.
    /// </summary>
    /// <typeparam name="TContext">type of DataContext to use</typeparam>
    /// <remarks>This currently only supports type int and string as its id and creator/updator respectively</remarks>
    public class HierarchyDataManager<TContext> : DataManager<TContext>
        where TContext : OContext
    {
        #region Count

        /// <summary>
        /// Returns the number of data found with the specified id.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="id">the id</param>
        /// <returns>the number of data found with the specified id</returns>
        public int Count<TTable>(int id)
            where TTable : HierarchyData<int, string>
        {
            return base.Count<TTable, int, string>(id);
        }

        /// <summary>
        /// Returns the number of data found under the specified parent id.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="parentId">the id of parent node</param>
        /// <returns>the number of data found under the specified parent id</returns>
        public int CountChildren<TTable>(int parentId)
            where TTable : HierarchyData<int, string>
        {
            return Context
                .GetTable<TTable>()
                .Where(d => d.ParentId.Equals(parentId))
                .Count();
        }

        /// <summary>
        /// Returns the number of data found matches with the specified id and parentId.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="id">the id</param>
        /// <param name="parentId">the id of parent node</param>
        /// <returns>the number of data found matches with the specified id and parent id</returns>
        public int Count<TTable>(int id, int parentId)
            where TTable : HierarchyData<int, string>
        {
            return Context
                .GetTable<TTable>()
                .Where(d => d.Id.Equals(id))
                .Where(d => d.ParentId.Equals(parentId))
                .Count();
        }

        #endregion

        #region Exists

        /// <summary>
        /// Returns a value indicating where the specified id exists in the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="id">the id to search for</param>
        /// <returns>true if exists, else false</returns>
        public bool Exists<TTable>(int id)
            where TTable : HierarchyData<int, string>
        {
            return base.Exists<TTable, int, string>(id);
        }

        /// <summary>
        /// Returns a value indicating where the specified name exists in the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="name">the name to search for</param>
        /// <returns>true if exists, else false</returns>
        public bool ExistsByName<TTable>(string name)
            where TTable : HierarchyData<int, string>
        {
            return base.ExistsByName<TTable, int, string>(name);
        }

        #endregion

        #region Retrieval

        /// <summary>
        /// Returns list of data matches with the specified id in the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="id">the id to search for</param>
        /// <returns>list of data matches with the specified id</returns>
        public List<TTable> Get<TTable>(int id)
            where TTable : HierarchyData<int, string>
        {
            return Context
                .GetTable<TTable>()
                .Where(d => d.Id.Equals(id))
                .ToList();
        }

        /// <summary>
        /// Returns the list of data matches with the specified id and parent id in the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="id">the id</param>
        /// <param name="parentId">the parent id</param>
        /// <returns>list of data matches with the specified id and parent id</returns>
        public List<TTable> Get<TTable>(int id, int parentId)
            where TTable : HierarchyData<int, string>
        {
            return Context
                .GetTable<TTable>()
                .Where(d => d.Id.Equals(id))
                .Where(d => d.ParentId.Equals(parentId))
                .ToList();
        }

        /// <summary>
        /// Returns data matches with the specified name in the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="name">the name to search for</param>
        /// <returns>data matches with the specified name from the table source. An exception is thrown if several data are found</returns>
        public TTable GetByName<TTable>(string name)
            where TTable : HierarchyData<int, string>
        {
            return base.GetByName<TTable, int, string>(name);
        }

        /// <summary>
        /// Returns list of data matches with the specified parent id from the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="parentId">the parent id</param>
        /// <returns>list of data matches with the specified parent id</returns>
        public List<TTable> GetByParentId<TTable>(int parentId)
            where TTable : HierarchyData<int, string>
        {
            return Context
                .GetTable<TTable>()
                .Where(d => d.ParentId.Equals(parentId))
                .ToList();
        }

        /// <summary>
        /// Returns list of records with the specified parent ids from the data source.
        /// </summary>
        /// <typeparam name="TTable">the table data type to search from</typeparam>
        /// <param name="parentIds">list of parent ids to search for</param>
        /// <returns>list of data matches with the specified parent ids</returns>
        public List<TTable> GetByParentIds<TTable>(params int[] parentIds)
            where TTable : HierarchyData<int, string>
        {
            parentIds.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "parentIds"));

            return Context
                .GetTable<TTable>()
                .Where(d => parentIds.Contains(d.ParentId))
                .ToList();
        }

        /// <summary>
        /// Returns list of child data matches with the specified parent id from the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="parentId">the parent id to search for</param>
        /// <param name="depth">depth to traverse</param>
        /// <returns>list of data matches with the specified parent id</returns>
        public List<TTable> GetChildren<TTable>(int parentId, int depth = 1)
            where TTable : HierarchyData<int, string>
        {
            // prepare variables
            List<int>    parentIds = new List<int>();
            List<TTable> children  = new List<TTable>();
            parentIds.Add(parentId);

            // traverse to specified depth
            for (int i = 0; i < depth; i++)
            {
                List<TTable> data = Context
                    .GetTable<TTable>()
                    .Where(d => parentIds.Contains(d.ParentId))
                    .ToList();
                if (data.Count == 0) // no children found
                {
                    break;
                }
                children.AddRange(data);

                if ((i + 1) < depth)
                {
                    parentIds.Clear();
                    parentIds.AddRange(data.AsEnumerable().Select(d => d.Id).ToList());
                }
            }

            return children;
        }

        /// <summary>
        /// Returns the data id with the specified name.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="name">name</param>
        /// <param name="ignoreCase">true, as default, to ignore case during the comparison; otherwise, false</param>
        /// <returns>the id or throws exception if multiple data found</returns>
        public int GetIdByName<TTable>(string name, bool ignoreCase = true)
            where TTable : HierarchyData<int, string>
        {
            return base.GetIdByName<TTable, int, string>(name, ignoreCase);
        }

        /// <summary>
        /// Returns list of parent data matches with the specified id from the table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="parentId">the id to search for</param>
        /// <param name="depth">depth to traverse</param>
        /// <returns>list of parent data matches with the specified id</returns>
        public List<TTable> GetParents<TTable>(int id, int depth = 1)
            where TTable : HierarchyData<int, string>
        {
            // prepare variables
            List<int>    ids     = new List<int>();
            List<TTable> parents = new List<TTable>();
            ids.Add(id);

            for (int i = 0; i <= depth; i++)
            {
                List<TTable> data = Context
                    .GetTable<TTable>()
                    .Where(d => ids.Contains(d.Id))
                    .ToList();
                if (data.Count == 0) // no parent found
                {
                    break;
                }

                // ONLY add parent nodes
                if (i > 0)
                {
                    parents.AddRange(data);
                }

                if ((i + 1) <= depth)
                {
                    ids.Clear();
                    ids.AddRange(data.AsEnumerable().Select(d => d.ParentId).ToList());
                }
            }

            return parents;
        }

        #endregion

        #region Insertion

        /// <summary>
        /// Creates and inserts new root node(s) with specified name(s) into the specified table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="names">list of names to use</param>
        /// <returns>an array of the newly-created data</returns>
        public TTable[] Insert<TTable>(params string[] names)
            where TTable : HierarchyData<int, string>
        {
            names.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "names"));

            // Creates data with the specified name(s)
            TTable[] data = new TTable[names.Length];

            for (int i = 0; i < names.Length; i++)
            {
                data[i]      = typeof(TTable).Instance<TTable>();
                data[i].Name = names[i];
            }

            return Insert<TTable>(data);
        }

        /// <summary>
        /// Inserts the specified array of data as root node(s) into the specified table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="data">an array of data to insert</param>
        /// <returns>an array of the newly-created data</returns>
        public TTable[] Insert<TTable>(params TTable[] data)
            where TTable : HierarchyData<int, string>
        {
            data.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "data"));

            base.Insert<TTable, int, string>(data);
            return data;
        }

        /// <summary>
        /// Creates and inserts new child node(s) with the specified names into the specified table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="parentId">the id of parent node</param>
        /// <param name="names">an array of names to insert</param>
        /// <returns>an array of the newly-created data</returns>
        public TTable[] InsertChildren<TTable>(int parentId, params string[] names)
            where TTable : HierarchyData<int, string>
        {
            names.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "names"));

            // Creates data with the specified name(s)
            TTable[] data = new TTable[names.Length];

            for (int i = 0; i < names.Length; i++)
            {
                data[i]      = typeof(TTable).Instance<TTable>();
                data[i].Name = names[i];
            }

            return InsertChildren<TTable>(parentId, data);
        }

        /// <summary>
        /// Inserts the specified array of data as child node(s) of specified parent id into the specified table source.
        /// </summary>
        /// <typeparam name="TTable">the table source</typeparam>
        /// <param name="parentId">the id of parent node</param>
        /// <param name="data">an array of data to insert</param>
        /// <returns>an array of the newly-created data</returns>
        public TTable[] InsertChildren<TTable>(int parentId, params TTable[] data)
            where TTable : HierarchyData<int, string>
        {
            data.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "data"));

            for (int i = 0; i < data.Length; i++)
            {
                data[i].ParentId = parentId;
            }

            base.Insert<TTable, int, string>(data);
            return data;
        }

        #endregion
    }
}
